package xueqiu

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"gitee.com/lyuanbo/stock-plus/pkg/cache/redisx"
	"gitee.com/lyuanbo/stock-plus/pkg/consts"
	"gitee.com/lyuanbo/stock-plus/pkg/logger"
	"gitee.com/lyuanbo/stock-plus/pkg/stock/types"
	"gitee.com/lyuanbo/stock-plus/pkg/stock/xueqiu/models"
	"github.com/go-redsync/redsync/v4"
	"github.com/google/uuid"
	"github.com/imroc/req/v3"
	"github.com/redis/go-redis/v9"
	"github.com/shopspring/decimal"
	"github.com/skip2/go-qrcode"
	"go.uber.org/zap"
	"golang.org/x/exp/slices"
	"golang.org/x/sync/errgroup"
)

const (
	redisKeyXueQiuAccessToken = "xq:access_token"
	redisKeyXueQiuUid         = "xq:uid"
)

var _ types.Stocker = (*XueQiu)(nil)

// XueQiu 是一个 XueQiu 客户端结构体。
type XueQiu struct {
	ctx    context.Context
	cancel context.CancelFunc
	token  string
	uid    int64

	req     *req.Client
	redis   *redis.Client
	redSync *redsync.Redsync
	logger  *logger.Logger

	mux              sync.Mutex
	symbols          []string
	symbolMap        map[string]*types.StockInfo
	symbolByExchange map[string][]string
	exchange         types.Exchanger
}

// New 是一个构造函数，用于创建一个新的 XueQiu 结构体实例
func New(ctx context.Context, redisClient *redis.Client, logger *logger.Logger) *XueQiu {
	xq := &XueQiu{
		ctx:              ctx,
		req:              req.NewClient(),
		redis:            redisClient,
		logger:           logger,
		symbolMap:        make(map[string]*types.StockInfo),
		symbolByExchange: make(map[string][]string),
	}

	xq.redSync = redisx.NewRedisSync(redisClient)

	xq.ctx, xq.cancel = context.WithCancel(ctx)

	uid, _ := uuid.NewV7()
	xq.req = req.C().SetCommonHeaders(map[string]string{
		"Accept":  "application/json, text/plain, */*",
		"Origin":  "https://xueqiu.com",
		"Referer": "https://xueqiu.com",
	}).SetCommonCookies(&http.Cookie{
		Name:  "device_id",
		Value: uid.String(),
	})

	xq.Login()
	xq.req.OnAfterResponse(func(client *req.Client, resp *req.Response) error {
		// fmt.Println("------------------")
		// fmt.Println(resp)
		return nil
	})
	return xq
}

func (xq *XueQiu) Realtime(symbols ...string) ([]*types.Kline, error) {
	// 优先symbols参数, 其次缓存, 防备出现数据重复
	if len(symbols) == 0 {
		symbols = xq.symbols
	}
	if len(symbols) == 0 {
		return nil, errors.New("symbols is empty")
	}

	// 每20个请求一次
	var (
		result []*types.Kline
		mux    sync.Mutex
		eg     errgroup.Group
	)

	for i := 0; i < len(symbols); i += 20 {
		end := i + 20
		if end > len(symbols) {
			end = len(symbols)
		}
		symbolsSlice := symbols[i:end]
		eg.Go(func() error {
			resp, err := xq.Quotec(symbolsSlice...)
			if err != nil {
				return err
			}

			for _, kline := range resp.Data {
				mux.Lock()
				result = append(result, &types.Kline{
					StockInfo: xq.symbolMap[strings.ToLower(kline.Symbol)],
					KlineInfo: types.KlineInfo{
						Timestamp:     time.UnixMilli(kline.Timestamp),
						Open:          kline.Open,
						High:          kline.High,
						Low:           kline.Low,
						Close:         kline.Current,
						LastClose:     kline.LastClose,
						Volume:        kline.Volume,
						Amount:        kline.Amount,
						TurnoverRate:  kline.TurnoverRate,
						Amplitude:     kline.Amplitude,
						Change:        kline.Chg,
						ChangePercent: kline.Percent,
					},
				})
				mux.Unlock()
			}
			return nil
		})
	}

	err := eg.Wait()
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (xq *XueQiu) Quotec(symbols ...string) (*models.QuoteCResponse, error) {
	// xq.req.R().SetQueryParam()
	return nil, nil
}

func (xq *XueQiu) WatchListStocks() {
	mux := xq.redSync.NewMutex("xueqiu:watchlist_stock", redsync.WithExpiry(time.Minute*5))
	if err := mux.Lock(); err != nil {
		time.Sleep(time.Second * 5)
	}

	defer func() { _, _ = mux.Unlock() }()

	result := xq.StockList("-1")
	if result == nil {
		return
	}

	positions := xq.StockList("7")
	if positions == nil {
		xq.logger.Warn(xq.ctx, "xueqiu: get positions failed")
	}

	positionsMap := make(map[string]bool)
	if positions != nil {
		for _, position := range positions.Data.Stocks {
			positionsMap[strings.ToLower(position.Symbol)] = true
		}
	}

	// 保存到redis
	pipe := xq.redis.Pipeline()
	for _, stock := range result.Data.Stocks {
		stock.Symbol = strings.ToLower(stock.Symbol)
		// 交易所代码
		pipe.SAdd(xq.ctx, consts.RedisKeyExchangeSymbols(stock.Exchange), stock.Symbol)
		pipe.SAdd(xq.ctx, consts.RedisKeyStockSymbols, stock.Symbol)

		pipe.HSet(xq.ctx, consts.RedisKeyStockInfo(stock.Symbol), map[string]interface{}{
			"symbol":   stock.Symbol,
			"exchange": strings.ToUpper(stock.Exchange),
			"name":     stock.Name,
			"is_held":  positionsMap[stock.Symbol],
			"type":     stock.Type,
		})

		xq.mux.Lock()
		xq.symbols = append(xq.symbols, stock.Symbol)
		xq.symbolByExchange[stock.Exchange] = append(xq.symbolByExchange[stock.Exchange], stock.Symbol)
		xq.symbolMap[stock.Symbol] = &types.StockInfo{
			Symbol:   stock.Symbol,
			Exchange: xq.exchange.Get(stock.Exchange),
			Name:     stock.Name,
			IsHeld:   positionsMap[stock.Symbol],
		}

		switch stock.Type {
		case 11:
			xq.symbolMap[stock.Symbol].Type = "个股"
		case 12:
			xq.symbolMap[stock.Symbol].Type = "指数"
		case 13:
			xq.symbolMap[stock.Symbol].Type = "ETF"
		}

		xq.mux.Unlock()
	}
	_, err := pipe.Exec(xq.ctx)
	if err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: save stock list to redis failed", zap.Error(err))
		return
	}
}

func (xq *XueQiu) StockList(pid string) *models.StockListResponse {
	resp, err := xq.req.R().SetQueryParams(map[string]string{
		"category": "1",
		"pid":      pid,
		"size":     "1000",
	}).Get("https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json")
	if err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: GET /v5/stock/portfolio/stock/list.json failed", zap.Error(err))
		return nil
	}
	var result models.StockListResponse
	if err := resp.Unmarshal(&result); err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: unmarshal response failed", zap.Error(err))
		return nil
	}

	if len(result.Data.Stocks) == 0 {
		xq.logger.Warn(xq.ctx, "xueqiu: stock list is empty")
		return nil
	}

	if result.ErrorCode != 0 {
		xq.logger.Warn(xq.ctx, "xueqiu: get stock list failed", zap.Error(errors.New(result.ErrorDescription)))
		return nil
	}

	return &result
}

func (xq *XueQiu) LiveNews() ([]models.ListNewsItem, error) {
	resp, err := xq.req.R().SetQueryParam("count", "5").Get("https://xueqiu.com/statuses/livenews/list.json")
	if err != nil {
		return nil, fmt.Errorf("xueqiu: GET /statuses/livenews/list.json failed: %w", err)
	}

	// 优化 Unmarshal 错误处理，避免覆盖原 err
	var result models.ListNewsResponse
	if err := resp.Unmarshal(&result); err != nil {
		return nil, models.ErrUnmarshalFailed
	}

	if len(result.Items) == 0 {
		return nil, nil
	}

	// 获取 maxId，错误处理优化
	maxId, err := xq.redis.Get(xq.ctx, "xq:news:max_id").Int()
	if err != nil && !errors.Is(err, redis.Nil) {
		// 只有非空错误才返回，redis.Nil表示key不存在，不是异常
		return nil, fmt.Errorf("xueqiu: get max_id from redis failed: %w", err)
	}
	if maxId == result.Items[0].Id {
		return nil, nil
	}

	// 设置maxId到redis
	if err := xq.redis.Set(xq.ctx, "xq:news:max_id", result.Items[0].Id, 0).Err(); err != nil {
		return nil, fmt.Errorf("xueqiu: set max_id to redis failed: %w", err)
	}

	var items []models.ListNewsItem
	for _, item := range result.Items {
		if item.Id > maxId {
			items = append(items, item)
		}
	}

	return items, nil
}

func (xq *XueQiu) Login() {
	// 尝试获取锁
	lockKey := "xq:login:lock"
	lock := xq.redSync.NewMutex(lockKey, redsync.WithExpiry(20*time.Second))
	if err := lock.Lock(); err != nil {
		xq.logger.Info(xq.ctx, "正在登录中，跳过", zap.String("lock_key", lockKey))
		return
	}
	defer func() { _, _ = lock.Unlock() }()

	xq.logger.Info(xq.ctx, "获取登录锁成功，开始执行登录流程", zap.String("lock_key", lockKey))

	// 先判断登录是否还有效
	accessToken, err := xq.redis.Get(xq.ctx, redisKeyXueQiuAccessToken).Result()
	if err != nil && !errors.Is(err, redis.Nil) {
		xq.logger.Error(xq.ctx, "获取access_token失败", zap.Error(err))
		return
	}

	if accessToken != "" {
		// 验证token是否有效
		resp, err := xq.req.R().SetCookies(&http.Cookie{
			Name:  "xq_a_token",
			Value: accessToken,
		}).Get("https://xueqiu.com/user/show.json")
		if err != nil {
			xq.logger.Error(xq.ctx, "xueqiu: GET /user/show.json failed", zap.Error(err))
			return
		}
		if resp.StatusCode == 200 {
			xq.token = accessToken

			uid, err := xq.redis.Get(xq.ctx, redisKeyXueQiuUid).Int64()
			if err != nil && !errors.Is(err, redis.Nil) {
				xq.logger.Error(xq.ctx, "获取access_token失败", zap.Error(err))
				return
			}
			xq.uid = uid

			// 登录成功,补充全局handler
			xq.req = xq.req.SetCommonCookies(&http.Cookie{
				Name:  "xq_a_token",
				Value: xq.token,
			}, &http.Cookie{
				Name:  "u",
				Value: fmt.Sprintf("%d", xq.uid),
			})

			xq.logger.Info(xq.ctx, "access_token有效，跳过登录", zap.String("access_token", accessToken))
			return
		}
		xq.logger.Info(xq.ctx, "access_token已失效", zap.String("access_token", accessToken))

		// 删除
		xq.redis.Del(xq.ctx, redisKeyXueQiuAccessToken)
	}

	resp, err := xq.req.R().Post("https://xueqiu.com/snb/provider/generate-qr-code")
	if err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: POST /snb/provider/generate-qr-code failed", zap.Error(err))
		return
	}

	var result models.QrGenerateResponse
	if err := resp.Unmarshal(&result); err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: unmarshal Login response failed", zap.Error(err))
		return
	}
	if result.ResultCode != 200 {
		xq.logger.Warn(xq.ctx, "xueqiu: Login failed", zap.Error(errors.New(result.Message)))
		return
	}

	// 将二维码保存到图片
	err = qrcode.WriteFile(result.Data.QrCode, qrcode.Medium, 256, "./qrcode.png")
	if err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: write qrcode to file failed", zap.Error(err))
		return
	}

	var eg errgroup.Group
	eg.Go(func() error {
		// 解析路径
		urlParsed, _ := url.Parse(result.Data.QrCode)

		for {
			resp, err := xq.req.R().
				SetQueryParam("code", urlParsed.Query().Get("code")).
				Get("https://xueqiu.com/snb/provider/query-qr-code-state")

			if err != nil {
				return fmt.Errorf("get qrcode state failed: %w", err)
			}

			var loginResp models.LoginResponse
			if err := resp.Unmarshal(&loginResp); err != nil {
				return fmt.Errorf("unmarshal Login response failed: %w", err)
			}

			// 二维码过期
			if loginResp.ResultCode != 200 && loginResp.ResultCode != 20134 {
				return fmt.Errorf("login failed: %w", errors.New(loginResp.Message))
			}

			if loginResp.Data.AccessToken != "" {
				xq.token = loginResp.Data.AccessToken
				xq.uid = int64(loginResp.Data.Uid)
				xq.redis.Del(xq.ctx, lockKey)
				xq.redis.Set(xq.ctx, redisKeyXueQiuAccessToken, xq.token, 0)
				xq.redis.Set(xq.ctx, redisKeyXueQiuUid, xq.uid, 0)

				xq.logger.Info(xq.ctx, "xueqiu: Login success", zap.String("access_token", xq.token))
				return nil
			}
			time.Sleep(time.Second * 2)
		}

	})

	if err := eg.Wait(); err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: GET /snb/provider/query-qr-code-state", zap.Error(err))
		return
	}

	// 删除图片
	_ = os.Remove("./qrcode.png")
}

func (xq *XueQiu) GetExchange(symbols ...string) map[string]types.Exchange {
	resp := xq.Quote(symbols...)
	if resp == nil {
		return nil
	}

	m := make(map[string]types.Exchange)
	for _, item := range resp.Data.Items {
		m[item.Quote.Exchange] = types.Exchange{
			Code:     item.Quote.Exchange,
			Status:   item.Market.Status,
			TimeZone: item.Market.TimeZone,
		}
	}

	return m
}

func (xq *XueQiu) SetExchange(exchange types.Exchanger) {
	xq.exchange = exchange
	xq.WatchListStocks()
}

func (xq *XueQiu) Quote(symbols ...string) *models.QuoteResponse {
	if len(symbols) == 0 {
		return nil
	}
	resp, err := xq.req.R().SetQueryParam("symbol", strings.Join(symbols, ",")).Get("https://stock.xueqiu.com/v5/stock/batch/quote.json")
	if err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: GET /v5/stock/quote.json failed", zap.Error(err))
		return nil
	}

	var result models.QuoteResponse
	if err := resp.Unmarshal(&result); err != nil {
		xq.logger.Warn(xq.ctx, "xueqiu: unmarshal response failed", zap.Error(err))
		return nil
	}

	if result.ErrorCode != 0 {
		xq.logger.Warn(xq.ctx, "xueqiu: get quote failed", zap.Error(errors.New(result.ErrorDescription)))
		return nil
	}

	return &result
}

func (xq *XueQiu) Kline(symbol string, periods []string) map[string][]*types.Kline {
	m := map[string][]*types.Kline{}
	var defaultPeriods = []string{"1m", "5m", "15m", "30m", "60m", "120m", "day", "week", "month", "quarter", "year"}
	for _, period := range periods {
		if !slices.Contains(defaultPeriods, period) {
			m[period] = nil
			continue
		}

		resp, err := req.C().SetCommonCookies(&http.Cookie{
			Name:  "xq_a_token",
			Value: xq.token,
		}, &http.Cookie{
			Name:  "u",
			Value: fmt.Sprintf("%d", xq.uid),
		}).R().SetQueryParams(map[string]string{
			"symbol":    strings.ToUpper(symbol),
			"begin":     fmt.Sprintf("%d", time.Now().UnixMilli()),
			"period":    period,
			"type":      "before",
			"count":     "-284",
			"indicator": "kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance",
		}).Get("https://stock.xueqiu.com/v5/stock/chart/kline.json")

		if err != nil {
			xq.logger.Warn(xq.ctx, "xueqiu: GET /v5/stock/chart/kline.json failed", zap.Error(err))
			m[period] = nil
			continue
		}

		var result models.KlineResponse
		if err := resp.Unmarshal(&result); err != nil {
			xq.logger.Warn(xq.ctx, "xueqiu: unmarshal response failed", zap.Error(err))
			m[period] = nil
			continue
		}

		if result.ErrorCode != 0 {
			xq.logger.Warn(xq.ctx, "xueqiu: get kline failed", zap.Error(errors.New(result.ErrorDescription)))
			m[period] = nil
			continue
		}
		klines := make([]*types.Kline, 0, len(result.Data.Item))
		for _, item := range result.Data.Item {
			kline := &types.Kline{
				StockInfo: xq.symbolMap[symbol],
				KlineInfo: types.KlineInfo{
					Timestamp:     time.Unix(int64(item[0]/1000), 0),
					Open:          decimal.NewFromFloat(item[2]),
					High:          decimal.NewFromFloat(item[3]),
					Low:           decimal.NewFromFloat(item[4]),
					Close:         decimal.NewFromFloat(item[5]),
					Volume:        int64(item[1]),
					Amount:        decimal.NewFromFloat(item[9]),
					TurnoverRate:  decimal.NewFromFloat(item[8]),
					Change:        decimal.NewFromFloat(item[6]),
					ChangePercent: decimal.NewFromFloat(item[7]),
				},
			}
			if len(klines) > 0 {
				kline.KlineInfo.LastClose = klines[len(klines)-1].KlineInfo.Close
			}

			// 防止过期
			// kline.StockInfo.GetExchange = exchange.Get(kline.StockInfo.GetExchange.Code)

			klines = append(klines, kline)
		}
		m[period] = klines
	}

	return m
}

func (xq *XueQiu) GetSymbols() []string {
	xq.mux.Lock()
	defer xq.mux.Unlock()
	return xq.symbols
}

func (xq *XueQiu) GetSymbolsByExchange(exchanges ...string) []string {
	if len(exchanges) == 0 {
		return nil
	}
	xq.mux.Lock()
	defer xq.mux.Unlock()

	if len(exchanges) == 1 {
		return xq.symbolByExchange[strings.ToUpper(exchanges[0])]
	}

	var symbols []string
	for _, ex := range exchanges {
		symbols = append(symbols, xq.symbolByExchange[strings.ToUpper(ex)]...)
	}

	return symbols
}

func (xq *XueQiu) GetStockInfo(symbol string) *types.StockInfo {
	xq.mux.Lock()
	defer xq.mux.Unlock()
	return xq.symbolMap[strings.ToLower(symbol)]
}
